-module(db).
-export([createNeighborList/2, startServers/2, stopServers/1, add/3, del/2, dump/1, getMax/1, getMin/1, flush/1, stop/1]).
-import(erlang, [display/1]).

% Exported helper functions.  Shouldn't be used by clients
-export([startAvgGossip/2, addNeighbors/2, createNeighborList_help/4, getMin_help/2, getMax_help/2, startServers_help/3, loop/1, processMsg/2, getMinFloatFromListSet/3, getMaxFloatFromListSet/3]).

createNeighborList(Start, Servers) ->
    ListLength = lists:flatlength(Servers),
    createNeighborList_help(Start, ListLength, Servers, []).

createNeighborList_help(Start, ListLength, _Servers, NeighborList) when Start == ListLength + 1 ->
    NeighborList;
createNeighborList_help(Start, ListLength, Servers, NeighborList) ->
    MatrixSize = round(math:sqrt(ListLength)),
    MatrixMod = Start rem MatrixSize,
    
    case MatrixMod /= 1 of
        true ->
            MinusOne = appendNeighborList([lists:nth(Start - 1 , Servers) | NeighborList]);
        false ->
            MinusOne = []
    end,
    case MatrixMod /= 0 of
        true ->
           case Start + 1 =< ListLength of
            true ->
                PlusOne = appendNeighborList([lists:nth(Start + 1 , Servers) | MinusOne]);
            false -> PlusOne = MinusOne
           end;
        false ->  
            PlusOne = MinusOne
    end,
    case Start + MatrixSize =< ListLength of
        true ->
            PlusMatrixSize = appendNeighborList([lists:nth(Start + MatrixSize, Servers) | PlusOne]);
        false -> PlusMatrixSize = PlusOne
    end,
    case Start - MatrixSize > 0 of
        true ->
            MinusMatrixSize = appendNeighborList([lists:nth(Start - MatrixSize, Servers) | PlusMatrixSize]);
        false -> MinusMatrixSize = PlusMatrixSize
    end,
    Server = lists:nth(Start, Servers),
    Server ! {addn, hd(MinusMatrixSize)},
    addNeighbors(Server, tl(MinusMatrixSize)),
    createNeighborList_help(Start + 1, ListLength, Servers, NeighborList).

addNeighbors(_Server, [])->
    ok;
addNeighbors(Server, NeighborList) ->
    Server ! {addn, hd(NeighborList)},
    addNeighbors(Server, tl(NeighborList)).

appendNeighborList(NeighborList) ->
    NeighborList.
    
% starts multiple instances of a server
startServers(Num, Offset) ->
    Created = 1 + Offset,
	startServers_help(Num, Created, []).

startServers_help(Num, Created, List) when Num == Created - 1 ->
	List;
startServers_help(Num, Created, List) ->
	Id = list_to_atom("server" ++ integer_to_list(Created)),
	Name = {Id, node()},
	start(Id),
	startServers_help(Num, Created + 1, [ Name | List ]).
	 	
% Stops a list of servers
stopServers([]) ->
	ok;
stopServers(Servers) ->
	hd(Servers) ! quit,
	stopServers(tl(Servers)).

% Gets the minimum value from all the servers
getMin(Servers) ->
	getMin_help(Servers, 1.0E45).

getMin_help([], Min) ->
	Min;
getMin_help(Servers, Min) ->
	MinServer = getMinFromServer(hd(Servers)),
	if
		MinServer < Min ->
			getMin_help(tl(Servers), MinServer);
		true ->
			getMin_help(tl(Servers), Min)
	end.

% Gets the maximum value from all the servers
getMax(Servers) ->
	getMax_help(Servers, -1.0E45).

getMax_help([], Max) ->
	Max;
getMax_help(Servers, Max) ->
	MaxServer = getMaxFromServer(hd(Servers)),
	if
		MaxServer > Max ->
			getMax_help(tl(Servers), MaxServer);
		true ->
			getMax_help(tl(Servers), Max)
	end.
    
% Starts a single instance of the server with the given Id
start(Id) ->
	Dict = dict:new(),
	NeighborNodes = [],
	DataStruct = {Id, Dict, NeighborNodes, average:new()},
	register(Id, spawn(db, loop, [DataStruct])),
	%spawn(db, startAvgGossip, [Id, element(4, DataStruct)]),
	ok.

% client functions to pass a message to the given server
add(Id, Tag, Float) ->
	Id ! {add, Tag, Float},
	ok.
	
del(Id, Tag) ->
	Id ! {del, Tag},
	ok.

getMaxFromServer(Id) ->
	Id ! {max, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

getMinFromServer(Id) ->
	Id ! {min, self()},
	receive
		Float when is_float(Float) ->
			Float
	end.

flush(Id) ->
	Id ! flush,
	ok.

dump(Id) ->
	Id ! {dump, self()},
	receive
		Dict ->
			Dict
	end.

stop(Id) ->
	Id ! quit,
	ok.

% Message loop for retrieving, processing, and stopping the server
loop(DataStruct) ->
	receive
		quit ->
			%io:format("~p\tShutting down~n", [element(1, DataStruct)]),
			ok;
		Msg ->
			loop(processMsg(Msg, DataStruct))
	end.

processMsg(Msg, DataStruct) ->
	Id = element(1, DataStruct),
	Dict = element(2, DataStruct),
	NeighborNodes = element(3, DataStruct),
	AvgStruct = element(4, DataStruct),
	
	case Msg of
		{add, Tag, Float} when is_float(Float) ->
			%io:format("~p\tAdding a float~p~n", [Id, Float]),	
			RetVal = {Id, dict:append(Tag, Float, Dict), NeighborNodes, AvgStruct};
		{add, Tag, FloatList} when is_list(FloatList) ->
			case dict:is_key(Tag, Dict) of
				true ->
					%io:format("~p\tKey ~p already exists", [Id, Tag]),
					RetVal = DataStruct;
				_Other ->
					%io:format("~p\tAdding list of floats~n", [Id]),
					RetVal = {Id, dict:append_list(Tag, FloatList, Dict), NeighborNodes, AvgStruct}
			end;
		{addn, Node} when is_tuple(Node) ->
			%io:format("~p\tAdding a neighbor node~p~n", [Id, Node]),
			RetVal = {Id, Dict, [Node | NeighborNodes], AvgStruct};
		{max, Proc} ->
			%io:format("~p Getting maximum value~n", [Id]),	
			Proc ! getMaxFloatFromListSet(dict:fetch_keys(Dict), Dict, -1.0E45),
			RetVal = DataStruct;
		{min, Proc} ->
			%io:format("~p Getting minimum value~n", [Id]),
			Proc ! getMinFloatFromListSet(dict:fetch_keys(Dict), Dict, 1.0E45),
			RetVal = DataStruct;
		{dump, Proc} ->
			Proc ! DataStruct,
			RetVal = DataStruct;
		% methods for calculating an average
		{avg, start} ->
			NewAvgStruct = average:incrTimeStamp(1, average:addFragmentsToAvgStruct(dict:fetch_keys(Dict), Dict, AvgStruct)),
			
			spawn(average, start, [Id, NeighborNodes, NewAvgStruct]),
			RetVal = {Id, Dict, NeighborNodes, NewAvgStruct};

		{avg, ReceivedAvgStruct} ->
		  % Add my keys to my average struct
		  NewAvgStruct1 = average:addFragmentsToAvgStruct(dict:fetch_keys(Dict), Dict, AvgStruct),
		  
		  % Merge my average structure with that which was given to me
			NewAvgStruct = average:merge(NewAvgStruct1, ReceivedAvgStruct),
			
			case AvgStruct == NewAvgStruct of
			  true ->
			    ToGossip = NewAvgStruct;
			  false ->
			    ToGossip = average:incrTimeStamp(1, NewAvgStruct)
			end,
			
			% Start a new gossip routine with the updated structure
			spawn(average, start, [Id, NeighborNodes, ToGossip]),
			
			% Set the return value of this function to the new average structure
			RetVal = {Id, Dict, NeighborNodes, ToGossip};
		{getavg, Proc} ->
			Avg = average:getCount(AvgStruct),
			if
				Avg > 0 ->
					Proc ! {average:getSum(AvgStruct) / Avg, average:getTimeStamp(AvgStruct)};
				true ->
					Proc ! unknown
			end,
			RetVal = DataStruct;
		% methods for updating a fragment
		{update, Tag, FloatList} ->
			% First, update my own dictionary
			case dict:is_key(Tag, Dict) of
				true ->
					io:format("~n~n~p\tUpdating key ~p~n~n", [Id, Tag]),
					NewDict = dict:append_list(Tag, FloatList, dict:erase(Tag, Dict)),
					RetVal = {Id, NewDict, NeighborNodes, AvgStruct},
					display(RetVal);
				_Other ->
					RetVal = DataStruct
			end,
			% Then, start the process to gossip with other nodes
			io:format("~p\tStarting gossip process~n", [Id]),
			spawn(fileupdate, start, [Id, NeighborNodes, Tag, FloatList]);
		{sizeN, Proc} ->
		  	Proc ! length(NeighborNodes),
		  	RetVal = DataStruct;
		% Methods for retrieving a fragment
		{retrieve, Tag, Proc} ->
		  % Check if I have the fragment
		  case dict:is_key(Tag, Dict) of
		    true ->
		      io:format("~p\tReturning key ~p~n", [Id, Tag]),
		      Proc ! {Tag, dict:fetch(Tag, Dict), Id},
		      RetVal = DataStruct;
		    _Other ->
		      io:format("~p\tDon't have key for ~p~n", [Id, Tag]),
		      spawn(fileretrieve, start, [NeighborNodes, Tag, Proc]),
		      RetVal = DataStruct
		  end;
		Msg ->
			io:format("~p\tUnknown message ~p~n", [Id, Msg]),
			RetVal = DataStruct
	end,
	RetVal.
			 
getMinFloatFromListSet([], _Dict, Min) ->
    Min;
getMinFloatFromListSet(Keys, Dict, Min) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMin = lists:min(Values),
    if
        ListMin < Min ->
            getMinFloatFromListSet(tl(Keys), Dict, ListMin);
        true ->
            getMinFloatFromListSet(tl(Keys), Dict, Min)
    end.
    
getMaxFloatFromListSet([], _Dict, Max) ->
    Max;
getMaxFloatFromListSet(Keys, Dict, Max) ->
    Key = hd(Keys),
    Values = dict:fetch(Key, Dict),
    ListMax = lists:max(Values),
    if
        ListMax > Max ->
            getMaxFloatFromListSet(tl(Keys), Dict, ListMax);
        true ->
            getMaxFloatFromListSet(tl(Keys), Dict, Max)
    end.

startAvgGossip(Proc, AvgStruct) ->

	random:seed(now()),
	Random = random:uniform(),

	if
		Random < 0.01 ->
			Proc ! {avg, start};
		true ->
			ok
	end,

	receive
	after 10 ->
		startAvgGossip(Proc, AvgStruct)
	end.




